#region File Description
//-----------------------------------------------------------------------------
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements
using System;
using Microsoft.Xna.Framework;
using AssetData;
#endregion

namespace Engine
{
    /// <summary>
    /// Helper for objects that want to leave particles behind them as they
    /// move around the world. This emitter implementation solves two related
    /// problems:
    /// 
    /// If an object wants to create particles very slowly, less than once per
    /// frame, it can be a pain to keep track of which updates ought to create
    /// a new particle versus which should not.
    /// 
    /// If an object is moving quickly and is creating many particles per frame,
    /// it will look ugly if these particles are all bunched up together. Much
    /// better if they can be spread out along a line between where the object
    /// is now and where it was on the previous frame. This is particularly
    /// important for leaving trails behind fast moving objects such as rockets.
    /// 
    /// This emitter class keeps track of a moving object, remembering its
    /// previous position so it can calculate the velocity of the object. It
    /// works out the perfect locations for creating particles at any frequency
    /// you specify, regardless of whether this is faster or slower than the
    /// game update rate.
    /// </summary>
    public class ParticleEmitter
    {
        /// <summary>
        /// Unique name to identify this emitter.
        /// </summary>
        public string ID = "None";

        private ParticleSystem particleSystem;
        private float timeBetweenParticles;
        public Vector3 PreviousPosition;
        private float timeLeftOver;
        // To allow the emitter to be create up front but only used when needed
        // attempt to avoid garbage.
        private bool active = false;
        public bool Active
        {
            get { return active; }
        }
        // When the effect will end.  Zero for continuous.
        private TimeSpan effectEndTime;


        /////////////////////////////////////////////////////////////////////
        //
        #region Motion
        //
        private static Vector3 startCentrePosition = Vector3.Zero;
        private static Vector3 lineAngle = new Vector3(-1f, 0.5f, 1f);
        private const float distanceAboveTheFloor = 3f;
        private const float circleRadius = 6f;
        //
        /// <summary>
        /// The number of particles emitted per second.
        /// </summary>
        private float particlesPerSecond = 30f;
        public float ParticlesPerSecond
        {
            get { return particlesPerSecond; }
            set
            {
                particlesPerSecond = value;
                ChangeParticlesPerSecond(particlesPerSecond);
            }
        }

        public enum EmitterMotion
        {
            Static,
            Line,
            Circle
        }
        /// <summary>
        /// The path of the emitter.
        /// </summary>
        private EmitterMotion motion = EmitterMotion.Static;
        public EmitterMotion Motion
        {
            get { return motion; }
            set { motion = value; }
        }

        public enum EmitterFlow
        {
            Continuous,
            Periodic
        }
        /// <summary>
        /// Do the particles come out evenly or in bursts.
        /// </summary>
        private EmitterFlow emitFlow = EmitterFlow.Continuous;
        public EmitterFlow EmitFlow
        {
            get { return emitFlow; }
            set { emitFlow = value; }
        }

        /// <summary>
        /// Speed and straight line direction of the emitter.
        /// </summary>
        private Vector3 emitterVelocity = Vector3.Zero;
        /// <summary>
        /// Speed of the emitter movement.
        /// For moving emitters.
        /// </summary>
        private float speedPerSecond = 1.0f;
        public float Speed
        {
            get { return speedPerSecond; }
            set
            {
                speedPerSecond = value;
                emitterVelocity = Vector3.Multiply(Vector3.Normalize(lineAngle), value);
            }
        }

        private TimeSpan emitEndTime = TimeSpan.Zero;
        private TimeSpan emitNextTime = TimeSpan.Zero;
        /// <summary>
        /// The number of seconds between emissions.
        /// For non-continuous emitters.
        /// </summary>
        private float emitFrequency = 2.0f;
        public float EmitFrequency
        {
            get { return emitFrequency; }
            set
            {
                emitFrequency = value;
                emitNextTime = TimeSpan.Zero;
            }
        }
        /// <summary>
        /// Default to emit for one frame.
        /// For non-continuous emitters.
        /// </summary>
        private float emitDuration = 0.01667f;
        public float EmitDuration
        {
            get { return emitDuration; }
            set
            {
                emitDuration = value;
                emitEndTime = TimeSpan.Zero;
            }
        }
        //
        /// <summary>
        /// Set to true to reset the emiter to the origin of the line.
        /// </summary>
        private bool lineRestart = true;
        private TimeSpan lineStartTime = TimeSpan.Zero;
        //
        private Vector3 CircularPosition(GameTimeMirror gameTime)
        {
            double angle = (double)gameTime.TotalGameTime.TotalSeconds * speedPerSecond;
            return new Vector3(
                ((float)Math.Sin(angle) * circleRadius) + startCentrePosition.X,
                distanceAboveTheFloor,
                ((float)Math.Cos(angle) * circleRadius) + startCentrePosition.Z);
        }

        private Vector3 LinePosition(GameTimeMirror gameTime)
        {
            if (lineRestart)
            {
                lineRestart = false;
                lineStartTime = gameTime.TotalGameTime;
                emitEndTime = gameTime.TotalGameTime + TimeSpan.FromSeconds(emitDuration);
                emitNextTime = gameTime.TotalGameTime + TimeSpan.FromSeconds(emitFrequency);
                return startCentrePosition;
            }
            float flightTime = (float)(gameTime.TotalGameTime - lineStartTime).TotalSeconds;
            return Vector3.Add(startCentrePosition, Vector3.Multiply(emitterVelocity, flightTime));
        }
        //
        #endregion
        //
        /////////////////////////////////////////////////////////////////////



        /// <summary>
        /// Constructs a new particle emitter object.
        /// </summary>
        public ParticleEmitter(string withName)
        {
            ID = withName;
        }

        /// <summary>
        /// Constructs a new particle emitter object with settings.
        /// </summary>
        public ParticleEmitter(string withName, 
            EmitterMotion motionType, 
            EmitterFlow flow, 
            float particlesCreatedPerSecond,
            float speedPerSec, 
            float frequency,
            float duration)
        {
            ID = withName;
            Motion = motionType;
            EmitFlow = flow;
            particlesPerSecond = particlesCreatedPerSecond;
            Speed = speedPerSec;
            EmitFrequency = frequency;
            EmitDuration = duration;
        }

        public void Activate(ParticleSystem particle)
        {
            Activate(particle, particlesPerSecond, startCentrePosition, TimeSpan.Zero);
        }

        /// <summary>
        /// Activate a particle emitter with a fixed duration.
        /// </summary>
        public void Activate(ParticleSystem particle, float partsPerSecond, Vector3 initialPosition, TimeSpan endTime)
        {
            particleSystem = particle;

            ChangeParticlesPerSecond(partsPerSecond);

            PreviousPosition = initialPosition;
            active = true;
            effectEndTime = endTime;
        }

        /// <summary>
        /// Speed up or slow down how many particles are emitted per second.
        /// </summary>
        public void ChangeParticlesPerSecond(float perSecond)
        {
            particlesPerSecond = perSecond;
            timeBetweenParticles = 1.0f / perSecond;
        }

        /// <summary>
        /// Inactive emitters are available for use by any particle.
        /// </summary>
        public void Deactivate()
        {
            active = false;
        }

        private bool IsEmitterEmitting(GameTimeMirror gameTime)
        {
            if (emitFlow == EmitterFlow.Continuous)
            {
                return true;
            }
            if (gameTime.TotalGameTime < emitEndTime)
            {
                return true;
            }
            return false;
        }


        /// <summary>
        /// Update the emitter using built in motion
        /// </summary>
        public void Update(GameTimeMirror gameTime, Random random)
        {
            if (!active)
            {
                return;
            }
            UpdateTimes(gameTime);
            if (IsEmitterEmitting(gameTime))
            {
                switch (motion)
                {
                    case EmitterMotion.Circle:
                        Update(gameTime, CircularPosition(gameTime), random);
                        break;
                    case EmitterMotion.Line:
                        Update(gameTime, LinePosition(gameTime), random);
                        break;
                    default:
                        Update(gameTime, startCentrePosition, random);
                        break;
                }
            }
        }

        /// <summary>
        /// Updates the emitter, creating the appropriate number of particles
        /// in the appropriate positions.
        /// The random needs to be supplied from the correct thread.
        /// </summary>
        public void Update(GameTimeMirror gameTime, Vector3 newPosition, Random random)
        {
            if (!active)
            {
                return;
            }
            if (effectEndTime != TimeSpan.Zero && gameTime.TotalGameTime > effectEndTime)
            {
                Deactivate();
                return;
            }

            // Work out how much time has passed since the previous update.
            float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

            if (elapsedTime > 0)
            {
                // Work out how fast we are moving.
                Vector3 velocity = (newPosition - PreviousPosition) / elapsedTime;

                // If we had any time left over that we didn't use during the
                // previous update, add that to the current elapsed time.
                float timeToSpend = timeLeftOver + elapsedTime;
                
                // Counter for looping over the time interval.
                float currentTime = -timeLeftOver;

                // Create particles as long as we have a big enough time interval.
                while (timeToSpend > timeBetweenParticles)
                {
                    currentTime += timeBetweenParticles;
                    timeToSpend -= timeBetweenParticles;

                    // Work out the optimal position for this particle. This will produce
                    // evenly spaced particles regardless of the object speed, particle
                    // creation frequency, or game update rate.
                    float mu = currentTime / elapsedTime;

                    Vector3 position = Vector3.Lerp(PreviousPosition, newPosition, mu);

                    // Create the particle.
                    particleSystem.AddParticle(position, velocity, random);
                }

                // Store any time we didn't use, so it can be part of the next update.
                timeLeftOver = timeToSpend;
            }

            PreviousPosition = newPosition;
        }

        private void UpdateTimes(GameTimeMirror gameTime)
        {
            if (gameTime.TotalGameTime > emitNextTime && emitDuration > 0)
            {
                emitEndTime = gameTime.TotalGameTime + TimeSpan.FromSeconds(emitDuration);
                emitNextTime = gameTime.TotalGameTime + TimeSpan.FromSeconds(emitFrequency);
                lineRestart = true;
            }
            if (emitFrequency > 0 && emitNextTime <= TimeSpan.Zero)
            {
                emitNextTime = gameTime.TotalGameTime + TimeSpan.FromSeconds(emitFrequency);
            }
            if (emitDuration > 0 && emitEndTime <= TimeSpan.Zero)
            {
                emitEndTime = gameTime.TotalGameTime + TimeSpan.FromSeconds(emitDuration);
            }
            if (lineStartTime <= TimeSpan.Zero)
            {
                lineStartTime = gameTime.TotalGameTime;
            }
        }

        /// <summary>
        /// Returns a copy of the emitter with the same name and settings.
        /// </summary>
        public ParticleEmitter Clone()
        {
            return new ParticleEmitter(ID, Motion, EmitFlow, ParticlesPerSecond, Speed, EmitFrequency, EmitDuration);
        }

        /////////////////////////////////////////////////////////////////////
        //
        #region Load and Save
        //
        public string ToEmitterString()
        {
            return ID +
                ParseData.Div + MotionToString(Motion) +
                ParseData.Div + FlowToString(EmitFlow) +
                ParseData.Div + ParseData.FloatToString(ParticlesPerSecond) +
                ParseData.Div + ParseData.FloatToString(Speed) +
                ParseData.Div + ParseData.FloatToString(EmitFrequency) +
                ParseData.Div + ParseData.FloatToString(EmitDuration);
        }

        public static ParticleEmitter FromEmitterString(string data)
        {
            if (string.IsNullOrEmpty(data))
            {
                return null;
            }
            string[] items = ParseData.SplitItemByDivision(data);
            if (items.Length != 7)
            {
                return null;
            }
            return new ParticleEmitter(items[0],
                MotionFromString(items[1]),
                FlowFromString(items[2]),
                ParseData.FloatFromString(items[3]),
                ParseData.FloatFromString(items[4]),
                ParseData.FloatFromString(items[5]),
                ParseData.FloatFromString(items[6]));
        }

        private const string motionLine = "LINE";
        private const string motionCircle = "CIRCLE";
        private const string motionStatic = "STATIC";

        public static string MotionToString(EmitterMotion data)
        {
            switch (data)
            {
                case EmitterMotion.Circle:
                    return motionCircle;
                case EmitterMotion.Line:
                    return motionLine;
            }
            return motionStatic;
        }

        public static EmitterMotion MotionFromString(string data)
        {
            switch (data)
            {
                case motionCircle:
                    return EmitterMotion.Circle;
                case motionLine:
                    return EmitterMotion.Line;
            }
            return EmitterMotion.Static;
        }

        private const string flowPeriodic = "PERIODIC";
        private const string flowContinuous = "CONTINUOUS";

        public static string FlowToString(EmitterFlow data)
        {
            switch (data)
            {
                case EmitterFlow.Periodic:
                    return flowPeriodic;
            }
            return flowContinuous;
        }

        public static EmitterFlow FlowFromString(string data)
        {
            switch (data)
            {
                case flowPeriodic:
                    return EmitterFlow.Periodic;
            }
            return EmitterFlow.Continuous;
        }
        //
        #endregion
        //
        /////////////////////////////////////////////////////////////////////

    }
}
